そのlistとdict、ひょっとするとtupleとnamedtupleで良いかも?

そのlistとdict、ひょっとするとtupleとnamedtupleで良いかも?

Clock Icon2024.08.31

こんちには。

データ事業本部 インテグレーション部 機械学習チームの中村( @nokomoro3 )です。

今回はPythonのlistとdictを変更できないデータにするために、tupleとnamedtupleを使用する方法をご紹介します。

なお、このお題は以下の書籍を参考にしています。

https://www.kadokawa.co.jp/product/302304004673/

そもそもなぜ変更できないデータにしたいか

代入後の変更などによって、意図しない変数を変更してしまうことを避けるためが、主な理由になるかと思います。

以下の例のようにhogeを代入したfugaに変更を加えると、hogeの方にも影響がでます。

hoge = ["A", "B", "C"]

fuga = hoge

fuga.append("D")
print(fuga)
# ['A', 'B', 'C', 'D']

print(hoge)
# ['A', 'B', 'C', 'D']

これは代入した場合でも、同じオブジェクトIDを指すためになります。

print(id(fuga))
# 132487865408064

print(id(hoge))
# 132487865408064

こちらが問題になるのは、listやdictとクラスなどのオブジェクトです。

intなどの場合も代入後は同じオブジェクトIDを指しますが、intはImmutableであるため、同じオブジェクトIDを維持したまま値を変更することはできません。

hoge = 100

fuga = hoge

print(id(fuga))
# 132488504823120

print(id(hoge))
# 132488504823120

fugaの値を変更してみます。(実際には変更はできないのですが)

fuga = 200

print(id(fuga))
# 132488504826320 # オブジェクトの再作成になるためこちらだけ変わる

print(id(hoge))
# 132488504823120

実際には変更できず、オブジェクトが再作成されているのが分かります。

つまり、dictとlistもImmutableなものに置き換えれば、こういった問題が発生しなくなります。これが今回の記事の動機です。

またcopyモジュールのdeepcopyを使って代入するようにすればこの問題は確かに解決しますが、通常の代入操作を禁止することまではできないため、安全性を保証するのは難しいと考えられます。

また、dict等は計算量を工夫するためにオーバーアロケートを行っていますが、変更できないデータにすると固定長をメモリとして確保すればよいため、メモリ効率の面でも優れているようです。

なお、余談ですがintの例でhogeの方を同じ値に書き換えると、同じオブジェクトIDをまた指すようになります。

hoge = 200

print(id(fuga))
# 132488504826320

print(id(hoge))
# 132488504826320

これは200という値自体が以下のオブジェクトIDを持つからです。

print(id(200))
# 132488504826320

listをtupleにする

以下のようにするだけでOKです。

hoge = ["A", "B", "C"]
hoge = tuple(hoge)
fuga = hoge

ただしtupleにはlistで良く使うappendがありません(Immutableになったため当然なのですが)。

fuga.append("D")

# AttributeError: 'tuple' object has no attribute 'append'

なので要素の追加を行う場合は、オブジェクトの再作成を行うような書き方となります。

これはアンパックをうまく使えばできます。

fuga = tuple([*fuga, "D"])
print(fuga)
# ('A', 'B', 'C', 'D')

tupleはlistと同じようにスライスすることが可能です。

print(fuga[1:])
# ('B', 'C', 'D')

アンパックもできます。

print(*fuga)
# A B C D

dictをnamedtupleにする

少し面倒なのですが、以下のように最初に名前付きタプルの型のようなものを作ってあげる必要があります。

hoge = {"aaa": 100, "bbb": 200, "ccc": 300}

from collections import namedtuple
Hoge = namedtuple("Hoge", hoge.keys())
hoge = Hoge(**hoge)

print(hoge)
# Hoge(aaa=100, bbb=200, ccc=300)

namedtupleの場合、dictでよくやるキーと値の追加ができなくなります。

hoge["ddd"] = 400
# TypeError: 'Hoge' object does not support item assignment

値へのアクセス方法もかわり、dictのような以下のアクセスはできません。

print(hoge["aaa"])
# TypeError: tuple indices must be integers or slices, not str

代わりのドットでアクセスすることができます。

print(hoge.aaa)
# 100

キーと値を追加するには名前付きタプルの型を再定義するところから必要です。


Hoge = namedtuple("Hoge", [*hoge._fields, "ddd"])
hoge = Hoge(**hoge._asdict(), ddd=400)

print(hoge)
# Hoge(aaa=100, bbb=200, ccc=300)

_asdict() でdict型に変更ができますので、それをうまく使います。
また fields でキー一覧を取得できます。

またnamedtupleのアンパック ** は使えないので、 _asdict() を介する必要があります。

namedtupleの詳細は以下もご確認ください。

https://qiita.com/Seny/items/add4d03876f505442136

複雑なデータクラスを書き換えるの面倒では

とここまで書いてきましたが、実際にもう少し複雑な階層構造をもつデータクラスなどを使い始めると、この辺りを実装するのも大変になります。

この場合は、標準ライブラリではないのですがPydanticの導入などが選択肢となります。

https://blog.nflabs.jp/entry/2023/07/25/100000

Pydanticのモデルで frozen=True とすることにより、Immutableにすることができるので、こちらの方が実用上の汎用性は高いと思います。

Pydanticであればデータ型のバリデーションも可能なため、こちらも機会があれば紹介したいと思います。

まとめ

こちらのようなお話を詳しく知りたい方は、ぜひ「エキスパートPythonプログラミング」の方を読んでみることをオススメします。

https://www.kadokawa.co.jp/product/302304004673/

本記事が皆様のご参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.